﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;

namespace SilverlightSphereControl
{
    public enum DispersalPattern
    {
        Row,
        Column,
        Checkered,
        Random,
        VerticalCarousel,
        HorizontalCarousel,
        Manual
    }

    public partial class SphereControl : Canvas
    {
        public SphereControl()
        {
            InitializeComponent();
            _dispersalPattern = DispersalPattern.Column;
            ItemOpacity = 1;
        }

        void SphereControl_LayoutUpdated(object sender, EventArgs e)
        {
            InitializeElementPositions(Children);
        }

        double _currentHRotation = 0;
        double _currentVRotation = 0;

        private double CenterX { get { return (double.IsNaN(this.Width) ? this.ActualWidth : this.Width) / 2.0; } }
        private double CenterY { get { return (double.IsNaN(this.Height) ? this.ActualHeight : this.Height) / 2.0; } }

        private const double MAX_VERTICAL_LIMIT = 60;
        private const double MIN_VERTICAL_LIMIT = -60;
        private const double VERTICAL_SPACE_BETWEEN_LIMITS = MAX_VERTICAL_LIMIT - MIN_VERTICAL_LIMIT;

        private DispersalPattern _dispersalPattern;
        public DispersalPattern DispersalPattern
        {
            get { return _dispersalPattern; }
            set
            {
                _dispersalPattern = value;
                InitializeElementPositions(Children);
            }
        }

        public double ItemOpacity { get; set; }
        public double Radius { get; private set; }
        public UIElement SelectedElement { get; private set; }


        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            InitializeElementPositions(Children);
            foreach (var child in Children)
            {
                child.Opacity = ItemOpacity;
            }
        }


        private void InitializeElementPositions(UIElementCollection children)
        {
            Radius = Math.Min(
                    double.IsNaN(this.Height) ? this.ActualHeight : this.Height,
                    double.IsNaN(this.Width) ? this.ActualWidth : this.Width) / 2;

            if (Children.Count > 0)
            {
                foreach (var child in children)
                {
                    child.MouseLeftButtonDown -= new MouseButtonEventHandler(NavigateToNewTarget);
                    child.MouseLeftButtonDown += new MouseButtonEventHandler(NavigateToNewTarget);

                    Canvas.SetLeft(child, CenterX - GetCenterX(child));
                    Canvas.SetTop(child, CenterY - GetCenterY(child));
                }

                switch (_dispersalPattern)
                {
                    case DispersalPattern.Random:
                        DisperseElementsRandomly(children);
                        break;
                    case DispersalPattern.VerticalCarousel:
                        PutInVerticalCarousel(children);
                        break;
                    case DispersalPattern.HorizontalCarousel:
                        PutInHorizontalCarousel(children);
                        break;
                    case DispersalPattern.Checkered:
                        OrderElementsInCheckeredPattern(children);
                        break;
                    case DispersalPattern.Manual:
                        if (children.Any(c => (c.Projection == null) || !(c.Projection is PlaneProjection)))
                            throw new NotInitializedException();
                        break;
                    case DispersalPattern.Row:
                        OrderElementsInRows(children);
                        break;
                    case DispersalPattern.Column:
                    default:
                        OrderElementsInColumns(children);
                        break;
                }
                if (DispersalPattern != DispersalPattern.Manual)
                    NavigateToNewTarget(Children[Children.Count / 2], null);
                else
                    NavigateToNewTarget(Children[0], null);
            }
        }

        private double GetCenterY(UIElement child)
        {
            if ((child is FrameworkElement) && (!double.IsNaN((child as FrameworkElement).Height)))
                {
                return (child as FrameworkElement).Height / 2.0;
            }
            else
            {
                return 0;
            }
        }

        private double GetCenterX(UIElement child)
        {
            if ((child is FrameworkElement) && (!double.IsNaN((child as FrameworkElement).Width)))
            {
                return (child as FrameworkElement).Width / 2.0;
            }
            else
            {
                return 0;
            }
        }

        private void OrderElementsInRows(UIElementCollection children)
        {
            int rows, columns, remainder;
            GetDimensionsForRows(children.Count, out rows, out columns, out remainder);
            if (remainder != 0)
                columns++;

            int numberOfHeightDivisions = rows <= 3 ? rows : rows - 2;

            for (int j = 0; j < columns; j++)
            {
                for (int i = 0; i < rows; i++)
                {
                    if (i * columns + j >= children.Count)
                        break;
                    PlaneProjection projection = new PlaneProjection();
                    projection.LocalOffsetZ = Radius;
                    projection.RotationY = j * (360.0 / columns);
                    projection.RotationX = MAX_VERTICAL_LIMIT - i * (VERTICAL_SPACE_BETWEEN_LIMITS / numberOfHeightDivisions);
                    children[i * columns + j].Projection = projection;
                }
            }
        }

        internal static void GetDimensionsForRows(int totalChildren, out int rows, out int columns, out int remainder)
        {
            int rowColumnRatioFactor = 4;
            int sqrt = (int)Math.Sqrt(totalChildren / rowColumnRatioFactor);

            rows = sqrt;
            columns = rowColumnRatioFactor * sqrt;
            remainder = totalChildren - rows * columns;
            while (remainder >= rows)
            {
                remainder -= rows;
                rows++;
            }
        }


        private void PutInVerticalCarousel(UIElementCollection children)
        {
            for (int i = 0; i < children.Count; i++)
            {
                PlaneProjection projection = new PlaneProjection();
                projection.LocalOffsetZ = Radius;
                projection.RotationX = MAX_VERTICAL_LIMIT - i * (VERTICAL_SPACE_BETWEEN_LIMITS / children.Count);
                children[i].Projection = projection;
            }
        }

        private void PutInHorizontalCarousel(UIElementCollection children)
        {
            for (int i = 0; i < children.Count; i++)
            {
                PlaneProjection projection = new PlaneProjection();
                projection.LocalOffsetZ = Radius;
                projection.RotationY = i * (360.0 / children.Count);
                children[i].Projection = projection;
            }
        }

        private void OrderElementsInCheckeredPattern(UIElementCollection children)
        {
            int rows, columns, remainder;
            GetDimensionsForColumns(children.Count, out rows, out columns, out remainder);
            if (remainder != 0)
                rows++;

            for (int i = 0; i < rows; i++)
            {
                double offset = (i % 2 == 0) ? 0 : 360.0 / (columns * 2);
                for (int j = 0; j < columns; j++)
                {
                    if (i * columns + j >= children.Count)
                        break;
                    PlaneProjection projection = new PlaneProjection();
                    projection.LocalOffsetZ = Radius;
                    projection.RotationY = j * (360.0 / (columns)) + offset;
                    projection.RotationX = MAX_VERTICAL_LIMIT - i * (VERTICAL_SPACE_BETWEEN_LIMITS / rows);
                    children[i * columns + j].Projection = projection;
                }
            }
        }

        private void DisperseElementsRandomly(UIElementCollection children)
        {
            double width = 360.0;
            double height = VERTICAL_SPACE_BETWEEN_LIMITS;
            Random rand = new Random();
            foreach (var child in children)
            {
                double proposedRotY = rand.NextDouble() * width;
                double proposedRotX = MAX_VERTICAL_LIMIT - rand.NextDouble() * height;

                // test for overlap of other children?

                child.Projection = new PlaneProjection() { };
                PlaneProjection projection = new PlaneProjection();
                projection.LocalOffsetZ = Radius;
                projection.RotationY = proposedRotY;
                projection.RotationX = proposedRotX;
                child.Projection = projection;
            }
        }

        private void OrderElementsInColumns(UIElementCollection children)
        {
            int rows, columns, remainder;
            GetDimensionsForColumns(children.Count, out rows, out columns, out remainder);

            if (remainder != 0)
                columns++;

            for (int j = 0; j < columns; j++)
            {
                for (int i = 0; i < rows; i++)
                {
                    if (i * columns + j >= children.Count)
                        break;
                    PlaneProjection projection = new PlaneProjection();
                    projection.LocalOffsetZ = Radius;
                    projection.RotationY = j * (360.0 / columns);
                    projection.RotationX = MAX_VERTICAL_LIMIT - i * (VERTICAL_SPACE_BETWEEN_LIMITS / rows);
                    children[i * columns + j].Projection = projection;
                }
            }
        }

        internal static void GetDimensionsForColumns(int totalChildren, out int rows, out int columns, out int remainder)
        {
            int sqrt = (int)Math.Sqrt(totalChildren);

            rows = columns = sqrt;

            remainder = totalChildren - sqrt * sqrt;
            while (remainder >= columns)
            {
                remainder -= columns;
                columns++;
            }
        }


        void NavigateToNewTarget(object sender, MouseButtonEventArgs e)
        {
            var target = (sender as FrameworkElement);
            if ((target != null) && (target.Projection as PlaneProjection != null))
            {
                Storyboard s = new Storyboard();
                s.Duration = new Duration(TimeSpan.FromMilliseconds(400));
                s.Completed += new EventHandler(RotationCompleted);

                double targetHRotation = ((PlaneProjection)target.Projection).RotationY;
                double deltaHRotation = _currentHRotation - targetHRotation;

                double targetVRotation = ((PlaneProjection)target.Projection).RotationX;
                double deltaVRotation = _currentVRotation - targetVRotation;

                foreach (var child in Children)
                {
                    PlaneProjection projection = child.Projection as PlaneProjection;

                    double currentHRotation = ((PlaneProjection)child.Projection).RotationY;
                    double newHRotation = currentHRotation + deltaHRotation;
                    DoubleAnimationUsingKeyFrames hAnimation = new DoubleAnimationUsingKeyFrames();
                    hAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = newHRotation, KeyTime = KeyTime.FromTimeSpan(s.Duration.TimeSpan), KeySpline = new KeySpline() { ControlPoint1 = new Point(0, 0), ControlPoint2 = new Point(.5, 1) } });
                    hAnimation.Duration = s.Duration;
                    s.Children.Add(hAnimation);
                    Storyboard.SetTarget(hAnimation, projection);
                    Storyboard.SetTargetProperty(hAnimation, new PropertyPath(PlaneProjection.RotationYProperty));

                    var currentVRotation = ((PlaneProjection)child.Projection).RotationX;
                    double newVRotation = currentVRotation + deltaVRotation;
                    DoubleAnimationUsingKeyFrames vAnimation = new DoubleAnimationUsingKeyFrames();
                    vAnimation.Duration = s.Duration;

                    if (newVRotation > MAX_VERTICAL_LIMIT)
                    {
                        double fullTimeSpan = s.Duration.TimeSpan.TotalMilliseconds;
                        double warpTimeFactor = Interpolate(currentVRotation, newVRotation, MAX_VERTICAL_LIMIT);
                        double warpTime = fullTimeSpan * warpTimeFactor;
                        TimeSpan warpTimeSpan = TimeSpan.FromMilliseconds(warpTime);

                        // disappear at top
                        vAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = MAX_VERTICAL_LIMIT, KeyTime = KeyTime.FromTimeSpan(warpTimeSpan) });

                        // reappear at bottom
                        vAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = MIN_VERTICAL_LIMIT, KeyTime = KeyTime.FromTimeSpan(warpTimeSpan) });

                        newVRotation -= VERTICAL_SPACE_BETWEEN_LIMITS;

                        DoubleAnimationUsingKeyFrames fadeAnimation = new DoubleAnimationUsingKeyFrames();
                        fadeAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = 0.0, KeyTime = KeyTime.FromTimeSpan(warpTimeSpan) });
                        fadeAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = ItemOpacity, KeyTime = KeyTime.FromTimeSpan(s.Duration.TimeSpan) });
                        fadeAnimation.Duration = s.Duration;
                        s.Children.Add(fadeAnimation);
                        Storyboard.SetTarget(fadeAnimation, child);
                        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath(UIElement.OpacityProperty));

                    }
                    else if (newVRotation < MIN_VERTICAL_LIMIT)
                    {
                        double fullTimeSpan = s.Duration.TimeSpan.TotalMilliseconds;
                        double warpTimeFactor = Interpolate(currentVRotation, newVRotation, MIN_VERTICAL_LIMIT);
                        double warpTime = fullTimeSpan * warpTimeFactor;
                        TimeSpan warpTimeSpan = TimeSpan.FromMilliseconds(warpTime);

                        // disappear at top
                        vAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = MIN_VERTICAL_LIMIT, KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(warpTime)) });

                        // reappear at bottom
                        vAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = MAX_VERTICAL_LIMIT, KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(warpTime)) });

                        newVRotation += VERTICAL_SPACE_BETWEEN_LIMITS;

                        DoubleAnimationUsingKeyFrames fadeAnimation = new DoubleAnimationUsingKeyFrames();
                        fadeAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = 0.0, KeyTime = KeyTime.FromTimeSpan(warpTimeSpan) });
                        fadeAnimation.KeyFrames.Add(new SplineDoubleKeyFrame() { Value = ItemOpacity, KeyTime = KeyTime.FromTimeSpan(s.Duration.TimeSpan) });
                        fadeAnimation.Duration = s.Duration;
                        s.Children.Add(fadeAnimation);
                        Storyboard.SetTarget(fadeAnimation, child);
                        Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath(UIElement.OpacityProperty));
                    }

                    vAnimation.KeyFrames.Add(new LinearDoubleKeyFrame() { Value = newVRotation, KeyTime = KeyTime.FromTimeSpan(s.Duration.TimeSpan) });
                    s.Children.Add(vAnimation);

                    Storyboard.SetTarget(vAnimation, projection);
                    Storyboard.SetTargetProperty(vAnimation, new PropertyPath(PlaneProjection.RotationXProperty));
                }

                SelectedElement = target;
                RotationStarting();
                s.Begin();
            }
        }


        public delegate void TargetChangingHandler(object sender, SelectionChangeEventArgs e);
        public event TargetChangingHandler TargetChanging;
        private void RotationStarting()
        {
            if (TargetChanging != null)
                TargetChanging(this, new SelectionChangeEventArgs() { Selected = SelectedElement });
        }

        public delegate void TargetChangedHandler(object sender, SelectionChangeEventArgs e);
        public event TargetChangedHandler TargetChanged;
        private void RotationCompleted(object sender, EventArgs e)
        {
            if (TargetChanged != null)
                TargetChanged(sender, new SelectionChangeEventArgs() { Selected = SelectedElement });
        }


   
        private double Interpolate(double min, double max, double inner)
        {
            if (max == min)
                return 0;
            return (inner - min) / (max - min);
        }

        private double DegreesToRads(double degrees)
        {
            return degrees * Math.PI / 180.0;
        }

        private void Canvas_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            InitializeElementPositions(Children);
        }

    }

    public class NotInitializedException : Exception
    {
        public override string Message
        {
            get
            {
                return "When using DispersalPattern.Manual, all children must have their Projection initialized to a PlaneProjection.";
            }
        }
    }

    public class SelectionChangeEventArgs : EventArgs
    {
        public UIElement Selected { get; set; }
    }
}
